using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using uTILLIty.Rave.Contract;
using uTILLIty.Rave.Contract.Configuration;
using uTILLIty.Rave.Contract.Exception;
using uTILLIty.Rave.Contract.StorageAdapter;
using uTILLIty.Rave.Core.Configuration;

namespace uTILLIty.Rave.Core.Strategy
{
	/// <summary>
	/// Handles all logic and interaction with the <see cref="IStorageAdapter"/> 
	/// </summary>
	public class DefaultConfigurationApplicatorStrategy : IConfigurationApplicatorStrategy
	{

		private ApplicatorContext _context;
		private readonly object _contextLock = new object();

		/// <summary>
		/// Required by unit-tests
		/// </summary>
		[Obsolete("Only provided for unit-tests!", false)]
		protected internal ApplicatorContext ApplicatorContext
		{
			get { return _context; }
			set { _context = value; }
		}

		/// <summary>
		/// Applies the <see cref="Strategy.ApplicatorContext">context</see> supplied to the target storage
		/// </summary>
		/// <param name="applicatorContext"></param>
		public void Apply(ApplicatorContext applicatorContext)
		{
			lock (_contextLock)
			{
				try
				{
					_context = applicatorContext;
					EnsureRootDirectoryExists();

					_context.Configuration.VisitDirectories(ProcessDirectory, OnVisitingError);
				}
				finally
				{
					_context = null;
				}
			}
		}

		/// <summary>
		/// Handler method for visiting errors
		/// </summary>
		/// <param name="visitingError">the error which occured</param>
		/// <seealso cref="Apply"/>
		protected virtual void OnVisitingError(ConfigurationExtensions.VisitingError visitingError)
		{
			visitingError.VisitChildren = false;
			_context.Job.TraceSource.TraceEvent(TraceEventType.Error, WellKnownEventIds.GeneralError,
				Properties.TraceMessages.ErrorProcessingDirectory, visitingError.DirectoryContext, visitingError.Exception.Message);
			visitingError.Rethrow = true;
		}

		/// <summary>
		/// Ensures that the root-directory exists in the underlying store
		/// </summary>
		/// <exception cref="ConfigurationErrorException">Raised, if the directory does not exist in the underlying store</exception>
		protected virtual void EnsureRootDirectoryExists()
		{
			var rootDir = _context.Configuration.RootDirectory;
			var errors = _context.StorageAdapter.ValidateRootPath(rootDir);
			if (errors != null && errors.Length > 0)
				throw new ConfigurationErrorException(Properties.ExceptionMessages.RootDirValidationError,
					rootDir, errors.Select(e => string.Format("\n- {0}", e)));

			var di = new DirectoryContext(new Directory(rootDir), null);
			if (_context.StorageAdapter.CheckDirectoryExists(di))
				return;

			if (_context.Configuration.CreateRootDirectory && !_context.SimulateOnly)
				_context.StorageAdapter.CreateDirectory(di);
			else if (!_context.SimulateOnly)
				throw new ConfigurationErrorException(string.Format(Properties.ExceptionMessages.RootDirMissing, rootDir));
		}

		/// <summary>
		/// Processes the Directory
		/// </summary>
		/// <param name="vc">the context of the visit</param>
		/// <seealso cref="Apply"/>
		protected virtual void ProcessDirectory(ConfigurationExtensions.VisitingContext vc)
		{
			DirectoryContext di = vc.DirectoryContext;
			bool exists = _context.StorageAdapter.CheckDirectoryExists(di);

			if (exists && di.Directory.SkipProcessingIfDirectoryExists)
			{
				TraceEvent(WellKnownEventIds.DirectoryCreation, Properties.TraceMessages.SkippingExistingDir, di);
				return;
			}

			TraceEvent(WellKnownEventIds.DirectoryCreation,
				exists ? Properties.TraceMessages.ProcessingExistingDir : Properties.TraceMessages.CreatingDir, di);

			if (!exists && !_context.SimulateOnly)
				_context.StorageAdapter.CreateDirectory(di);

			var inheritanceEnabled = _context.StorageAdapter.IsSecurityEntriesInheritanceEnabled(di);
			if (di.Directory.RemoveSecurityEntriesInheritance && inheritanceEnabled)
			{
				TraceEvent(WellKnownEventIds.RightsApplication, Properties.TraceMessages.RemovingSecurityInheritanceFromDir, di);
				if (!_context.SimulateOnly)
					_context.StorageAdapter.SetSecurityEntriesInheritance(di, true);
			}

			vc.VisitChildren = ProcessSecurity(di);
		}

		/// <summary>
		/// Processes the security of the directory
		/// </summary>
		/// <param name="di">the directory to process</param>
		/// <returns>False, if any errors occured, else True</returns>
		protected internal virtual bool ProcessSecurity(DirectoryContext di)
		{
			bool containsInheritedAcls;
			List<SecurityEntryContext> securityEntryContexts = _context.StorageAdapter.GetExistingSecurityEntries(di, out containsInheritedAcls);

			foreach (SecurityEntry securityEntry in di.Directory.SecurityEntries)
			{
				Member configMember = GetConfigMember(securityEntry);
				StorageMember storageMember = TryResolveStorageMember(configMember) ?? TryCreateMemberIfNotSimulating(configMember);

				if (storageMember == null)
				{
					TraceEvent(WellKnownEventIds.MemberCreation, Properties.TraceMessages.MemberNotResolvedOrCreated, configMember, di);
					return false;
				}

				// resolution of StorageMember already triggered creation of the member, so nothing else to do, if DontAddToAcl is true
				if (securityEntry.DontAddToAcl)
					continue;

				var existingSecurityEntryContext = GetOrCreateSecurityEntryContext(securityEntry, configMember, storageMember,
																																					 securityEntryContexts);
				if (existingSecurityEntryContext.Action != SecurityEntryAction.Add)
					UpdateSecurityEntryContext(existingSecurityEntryContext, securityEntry, storageMember);
				existingSecurityEntryContext.Touched = true;
			}

			if (di.Directory.RemoveAllOtherSecurityEntries)
			{
				var unprocessedExistingSecurityEntries = securityEntryContexts.Where(se => !se.Touched);
				foreach (SecurityEntryContext existingSecurityEntry in unprocessedExistingSecurityEntries)
				{
					existingSecurityEntry.Action = SecurityEntryAction.Remove;
				}
			}

			List<SecurityEntryContext> securityChangesToApply = securityEntryContexts.Where(se => se.Action != SecurityEntryAction.Nothing).ToList();
			foreach (var securityEntry in securityChangesToApply)
			{
				TraceEvent(WellKnownEventIds.RightsApplication, Properties.TraceMessages.ApplyingAclAction,
					securityEntry.StorageMember, securityEntry.Action, securityEntry.TemplateSecurityEntry);
			}
			if (!_context.SimulateOnly)
				_context.StorageAdapter.ApplySecurityChanges(di, securityChangesToApply);

			return true;
		}

		/// <summary>
		/// Locates a <see cref="SecurityEntryContext"/> by same <paramref name="storageMember"/> and security-inheritance/propagation attributes.
		/// If no match is found, a new instance is created
		/// </summary>
		/// <param name="securityEntry"></param>
		/// <param name="configMember"></param>
		/// <param name="storageMember"></param>
		/// <param name="securityEntryContexts"></param>
		/// <returns></returns>
		protected virtual SecurityEntryContext GetOrCreateSecurityEntryContext(SecurityEntry securityEntry, Member configMember, StorageMember storageMember, List<SecurityEntryContext> securityEntryContexts)
		{
			var temporaryContext = new SecurityEntryContext(storageMember, securityEntry.InheritanceFlags, securityEntry.PropagationFlags) { AccessControlType = securityEntry.AccessControlType };

			var securityEntryContext = securityEntryContexts.FirstOrDefault(sec => sec.Equals(temporaryContext));
			if (securityEntryContext == null)
			{
				securityEntryContext = temporaryContext;

				_context.Job.TraceSource.TraceEvent(TraceEventType.Verbose, WellKnownEventIds.RightsApplication,
																						Properties.TraceMessages.CreatingNewAclEntry, storageMember);
				securityEntryContext.Action = SecurityEntryAction.Add;
				securityEntryContexts.Add(securityEntryContext);
			}

			if (securityEntryContext.TemplateSecurityEntry == null)
				securityEntryContext.TemplateSecurityEntry = securityEntry;

			return securityEntryContext;
		}

		/// <summary>
		/// Updates the ACLs
		/// </summary>
		/// <param name="securityEntryContext"></param>
		/// <param name="securityEntry"></param>
		/// <param name="storageMember"></param>
		protected void UpdateSecurityEntryContext(SecurityEntryContext securityEntryContext, SecurityEntry securityEntry, StorageMember storageMember)
		{
			_context.Job.TraceSource.TraceEvent(TraceEventType.Verbose, WellKnownEventIds.RightsApplication,
				Properties.TraceMessages.ProcessingExistingAclEntry, securityEntryContext.StorageMember);

			var rightsDiffer = _context.StorageAdapter.DoRightsDiffer(securityEntryContext.CurrentRights, securityEntry.Rights);

			if (!rightsDiffer)
				return;

			switch (securityEntryContext.Action)
			{
				case SecurityEntryAction.Add:
				case SecurityEntryAction.Update:
					securityEntryContext.CurrentRights = _context.StorageAdapter.MergeRights(securityEntryContext.CurrentRights, securityEntry.Rights);
					break;
				case SecurityEntryAction.Nothing:
				case SecurityEntryAction.Remove:
					securityEntryContext.CurrentRights = securityEntry.Rights;
					securityEntryContext.Action = SecurityEntryAction.Update;
					break;
			}
		}

		/// <summary>
		/// Creates the member in the underlying store, or returns a fake <see cref="StorageMember"/>, if simulating
		/// </summary>
		/// <param name="member">the member to create</param>
		/// <returns>a new <see cref="StorageMember"/> instance of the member created</returns>
		protected virtual StorageMember TryCreateMemberIfNotSimulating(Member member)
		{
			StorageMember storageMember = _context.SimulateOnly
				? new StorageMember(member.Identifier, string.Format("SimulatedAdHocStorageMember [{0}]", member))
				: TryCreateStorageMember(member);

			if (storageMember != null)
				AddStorageMemberToCache(member, storageMember);

			return storageMember;
		}

		readonly Dictionary<Member, StorageMember> _storageMemberCache = new Dictionary<Member, StorageMember>(50);
		/// <summary>
		/// Retrieves a <see cref="StorageMember"/> from cache, or from the adapter, if not cached
		/// </summary>
		/// <param name="member">the member to resolve</param>
		/// <returns>The located <see cref="StorageMember"/></returns>
		protected virtual StorageMember TryResolveStorageMember(Member member)
		{
			StorageMember storageMember = TryGetStorageMemberFromCache(member);
			if (storageMember != null)
				return storageMember;

			storageMember = _context.StorageAdapter.TryResolveMember(member);

			if (storageMember != null)
				AddStorageMemberToCache(member, storageMember);

			return storageMember;
		}

		/// <summary>
		/// Creates the supplied <paramref name="member"/> in the underlying store
		/// </summary>
		/// <param name="member">the member to create</param>
		/// <returns>a reference to the newly created member</returns>
		protected virtual StorageMember TryCreateStorageMember(Member member)
		{
			TraceEvent(WellKnownEventIds.MemberCreation, Properties.TraceMessages.CreatingMember, member);
			StorageMember storageMember = _context.StorageAdapter.CreateMember(member);

			return storageMember;
		}

		/// <summary>
		/// Adds the <paramref name="storageMember"/> associated with the <paramref name="member"/> to the cache
		/// </summary>
		/// <param name="member">the member being referenced</param>
		/// <param name="storageMember">the <see cref="StorageMember"/> associated with the <paramref name="member"/></param>
		protected virtual void AddStorageMemberToCache(Member member, StorageMember storageMember)
		{
			lock (_storageMemberCache)
			{
				_context.Job.TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Adding StorageMember {0} (Member {1}) to cache",
					storageMember, member);
				_storageMemberCache.Add(member, storageMember);
			}
		}

		/// <summary>
		/// Tries to locate the supplied <paramref name="member"/> in cache
		/// </summary>
		/// <param name="member">the member to locate the <see cref="StorageMember"/> data for</param>
		/// <returns>the <see cref="StorageMember"/> found in cache, or Null, if not found</returns>
		protected virtual StorageMember TryGetStorageMemberFromCache(Member member)
		{
			lock (_storageMemberCache)
			{
				if (!_storageMemberCache.ContainsKey(member))
					return null;
				var storageMember = _storageMemberCache[member];
				_context.Job.TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Returning StorageMember {0} (Member {1}) from cache",
					storageMember, member);
				return storageMember;
			}
		}

		/// <summary>
		/// Returns the member associated with the supplied <paramref name="securityEntry"/>
		/// </summary>
		/// <param name="securityEntry">the security-entry to locate the member for</param>
		/// <returns>The found member, of null, if it could not be located</returns>
		protected virtual Member GetConfigMember(SecurityEntry securityEntry)
		{
			return ResolveMember(securityEntry.MemberID);
		}

		/// <summary>
		/// Logs a message to the TraceSource of the <see cref="Strategy.ApplicatorContext"/>
		/// </summary>
		/// <param name="eventId"></param>
		/// <param name="message"></param>
		/// <param name="args"></param>
		protected virtual void TraceEvent(int eventId, string message, params object[] args)
		{
			var eventType = TraceEventType.Verbose;
			if (eventId >= 4000) eventType = TraceEventType.Critical;
			else if (eventId >= 3000) eventType = TraceEventType.Error;
			else if (eventId >= 2000) eventType = TraceEventType.Warning;
			else if (eventId >= 1000) eventType = TraceEventType.Information;

			_context.Job.TraceSource.TraceEvent(eventType, eventId, message, args ?? new object[0]);
		}

		#region Implementation of IMemberResolver

		/// <summary>
		/// Resolves a member-configuration by ID
		/// </summary>
		/// <exception cref="ConfigurationErrorException">If specified member could not be located in configuration</exception>
		/// <exception cref="InvalidOperationException">Raised, if the <see cref="Strategy.ApplicatorContext"/> is not set</exception>
		public Member ResolveMember(string memberId)
		{
			if (_context == null)
				throw new InvalidOperationException(Properties.ExceptionMessages.ContextMissing);

			var member = _context.Configuration.Members
				.SingleOrDefault(m => m.ID.Equals(memberId, StringComparison.InvariantCultureIgnoreCase));
			if (member != null)
				return member;
			throw new ConfigurationErrorException(Properties.ExceptionMessages.MemberNotFound, memberId);
		}

		/// <summary>
		/// Resolves a member's details from cache or store
		/// </summary>
		public StorageMember ResolveStorageMember(Member member, bool tryCreate)
		{
			if (_context == null)
				throw new InvalidOperationException(Properties.ExceptionMessages.ContextMissing);

			var storageMember = TryResolveStorageMember(member);
			if (storageMember == null && tryCreate)
				storageMember = TryCreateMemberIfNotSimulating(member);

			return storageMember;
		}

		#endregion
	}
}